#!/usr/bin/python

"""IEEE 1212/ IEEE 1394 Configuration ROM pretty printer

Copyright 2010 Stefan Richter <stefanr@s5r6.in-berlin.de>
You may freely use, modify, and/or redistribute this program.

Reads Configuration ROM data from stdin and writes a human-readable
annotated representation to stdout.  The data may be
  - binary data, i.e. a big endian or little endian quadlet array,
  - a firewire-ohci debug log,
  - read results from the tool firecontrol.

Usage examples:

  - Read a configuration ROM as seen in sysfs:
    (plug device in, find out which fw* it became)
    crpp < /sys/bus/firewire/devices/fw1/config_rom

  - Read a debug log from when the drivers attempt to probe the device:
    echo 1 > /sys/module/firewire_ohci/parameters/debug
    (plug device in, wait a few seconds)
    dmesg | crpp
    echo 0 > /sys/module/firewire_ohci/parameters/debug

  - Read the configuration ROM by means of firecontrol.  The example
    assumes that the device has the node ID ffc0 and that it is
    attached to the first of all installed FireWire controllers:
    { for i in {4,5,6,7}{0,1,2,3,4,5,6,7,8,9,a,b,c,d,e,f}{0,4,8,c};
      do echo "r . 0 0xfffff0000$i 4";
      done } | firecontrol | crpp

If you want company IDs being translated to names, you need a file
called oui.db in /usr/share/misc/ or in the current working directory.
This file can be obtained from linux-2.6.20 or older kernel soures in
linux/drivers/ieee1394/ or can be freshly generated by
    wget -O - http://standards.ieee.org/regauth/oui/oui.txt |
    grep -E '(base 16).*\w+.*$' |
    sed -e 's/\s*(base 16)\s*/ /' > oui.db
which will download ~2 MB data and result in a ~0.4 MB large oui.db.

History:
2010-02-16: initial release
2010-02-18: small fixes; added protocol entries, OUI lookup, CRC checks
2010-04-25: added firecontrol input
2010-07-19: added vendor-defined specifier/version IDs
2012-04-23: added IIDC2 v1.0.0 protocol entries
"""

ouidb_search_paths = (
    "/usr/share/misc/oui.db",
    "oui.db",
)

def crc16(rom, i, l):
    def nextcrc(c, q):
        for j in xrange(28, -1, -4):
            s = (c >> 12 ^ q >> j) & 0xf
            c = c << 4 ^ s << 12 ^ s << 5 ^ s
        return c & 0xffff
    return reduce(nextcrc, rom[i:min(i + l, 256)], 0)

def u8_to_char(n):
    # separator/ terminator
    if n == 0:
        return ""
    # printable character from minimal ASCII
    if n in range(0x20, 0x23) + range(0x25, 0x5b) + [0x5f] + range(0x61, 0x7b):
        return chr(n)
    # other character
    return "~"

def u32_to_string(n):
    if n == 0:
        return ""
    s = "\""
    for i in 24, 16, 8, 0:
        s += u8_to_char(n >> i & 0xff)
    # add separator in c0c0, c0cc, c00c, or 0c0c
    if n & 0xff000000 and n & 0x0000ffff and not n & 0x00ff0000 or \
       n & 0x00ff0000 and n & 0x000000ff and not n & 0xff00ff00:
        s = s[:2] + "\", \"" + s[2:]
    # add separator in cc0c
    if n & 0xff000000 and n & 0x00ff0000 and n & 0xff and not n & 0xff00:
        s = s[:3] + "\", \"" + s[3]
    return s + "\""

def language(n):
    n &= 0xffff
    if n == 0:
        return "en"
    s = ""
    for i in 10, 5, 0:
        c = n >> i & 0x1f
        s += char(ord("a") - 1 + c) if c >= 1 and c <= 26 else " "
    return s

def bcd_version_details(n):
    return "v%s%d.%d%s" % (
        "%d" % v >> 20 & 0xf if v & 0xf00000 else "", v >> 16 & 0xf,
        v >> 12 & 0xf, "%d" % v >> 8 & 0xf if v & 0xf00 else ""),

def csr_address(offset):
    return 0xfffff0000000 + 4 * offset

dpp111_protocol_entries = {
    0x38: lambda v: "command set spec id" + (": " +
        protocols[v][0] if v in protocols else ""),
    0x39: lambda v: "command set" + {0xb081f2: ": DPC",
                                     0x020000: ": FTC"}.get(v, ""),
    0x3a: lambda v: "command set details",
    0x3c: lambda v: "write transaction interval %dms" % (v),
    0x3d: lambda v: "unit sw details: %s, sdu_write_order %d" % (
        bcd_version_details(v), v & 1),
    0x7b: lambda v: "connection CSR at %012x" % (csr_address(v)),
    0xd4: lambda v: "command set directory",
}

iicp_protocol_entries = {
    0x38: lambda v: "IICP details: %s" % (bcd_version_details(v)),
    0x39: lambda v: "command set spec id" + (": " +
        protocols[v][0] if v in protocols else ""),
    0x3a: lambda v: "command set" + {0x4b661f: ": IICP only",
                                     0xc27f10: ": IICP488"}.get(v, ""),
    0x3b: lambda v: "command set details: %s" % (bcd_version_details(v)),
    0x3d: lambda v: "IICP capabilities: hl proto %d, IICP %d, " \
        "ccli %d, cmgr %d, maxIntLength %s" % (
        v >> 16, v >> 6 & 0x3f, v >> 5 & 1, v >> 4 & 1,
        "%d bytes" % (2 << (v & 0xf)) if v & 0xf else "-"),
    0x7c: lambda v: "connection CSR at %012x" % (csr_address(v)),
    0x7e: lambda v: "interrupt_enable CSR at %012x" % (csr_address(v)),
    0x7f: lambda v: "interrupt_handlr CSR at %012x" % (csr_address(v)),
}

iidc104_protocol_entries = {
    0x40: lambda v: "command_regs_base at %012x" % (csr_address(v)),
    0x81: lambda v: "vendor name leaf",
    0x82: lambda v: "model name leaf",
}

iidc131_protocol_entries = {
    0x38: lambda v: "unit sub sw version v1.3%d" % (v >> 4),
    0x39: lambda v: "(reserved)",
    0x3a: lambda v: "(reserved)",
    0x3b: lambda v: "(reserved)",
    0x3c: lambda v: "vendor_unique_info_0",
    0x3d: lambda v: "vendor_unique_info_1",
    0x3e: lambda v: "vendor_unique_info_2",
    0x3f: lambda v: "vendor_unique_info_3",
    0x40: lambda v: "command_regs_base at %012x" % (csr_address(v)),
    0x81: lambda v: "vendor name leaf",
    0x82: lambda v: "model name leaf",
}

iidc2_100_protocol_entries = {
    0x38: lambda v: "unit sub sw version v%d.%d.%d" % (
        v >> 16, v >> 8 & 0xff, v & 0xff),
    0x40: lambda v: "IIDC2Entry at %012x" % (csr_address(v)),
    0x81: lambda v: "vendor name leaf",
    0x82: lambda v: "model name leaf",
}

isight_audio_protocol_entries = {
    0x40: lambda v: "register file at %012x" % (csr_address(v)),
}

isight_iris_protocol_entries = {
    0x40: lambda v: "Iris Status Address register at %012x" % (csr_address(v)),
}

sbp3_protocol_entries = {
    0x14: lambda v: "logical unit number: %sordered %d, %s" \
        "type %s, lun %04x" % (
        ("", "extended_status 1, ")[v >> 23 & 1], v >> 22 & 1,
        ("", "isoch 1, ")[v >> 21 & 1], {
            0x00: "Disk", 0x01: "Tape", 0x02: "Printer", 0x03: "Processor",
            0x04: "WORM", 0x05: "CD/DVD", 0x06: "Scanner", 0x07: "MOD",
            0x08: "Changer", 0x09: "Comm", 0x0a: "Prepress", 0x0b: "Prepress",
            0x0c: "RAID", 0x0d: "Enclosure", 0x0e: "RBC", 0x0f: "OCRW",
            0x10: "Bridge", 0x11: "OSD", 0x12: "ADC-2",
            0x1e: "w.k.LUN", 0x1f: "unnown",
        }.get(v >> 16 & 0x1f, "%02x?" % (v >> 16 & 0x1f)), v & 0xffff),
    0x21: lambda v: "revision %d%s" % (v,
        (" = SBP-2", " = SBP-3")[v] if v < 2 else ""),
    0x32: lambda v: "/ SBP-3 plug control register: %sPCR, " \
        "plug_index %d" % (("i", "o")[v >> 5 & 1], v & 0x1f),
    0x38: lambda v: "command set spec id" + (": " +
        protocols[v][0] if v in protocols else ""),
    0x39: lambda v: "command set" + {
            0x0104d8: ": SCSI Primary Commands 2 and related standards",
            0x010001: ": AV/C",
        }.get(v, ""),
    0x3a: lambda v: "unit char.: %smgt_ORB_timeout %gs, ORB_size " \
        "%d quadlets" % (("", "distrib. data 1, ")[v >> 16 & 1],
        (v >> 8 & 0xff) * .5, v & 0xff),
    0x3b: lambda v: "command set revision",
    0x3c: lambda v: "firmware revision %06x" % (v),
    0x3d: lambda v: "reconnect timeout: max_reconnect_hold %ds" % ((v & 0xffff) + 1),
    0x3e: lambda v: "/ SBP-3 fast start: max_payload " + (
        "%d bytes" % (v >> 8 << 2) if v & 0xff00
        else "per max_rec") + (", offset %d" % (v & 0xff)),
    0x54: lambda v: "management agent CSR at %012x" % (csr_address(v)),
    0x8d: lambda v: "unit unique id",
    0xd4: lambda v: "logical unit directory",
}

protocols = {
    # standardized
    0x00005e: ("IANA", {
        0x000001: ("IPv4 over 1394 (RFC 2734)", {}),
        0x000002: ("IPv6 over 1394 (RFC 3146)", {}),
    }),
    0x00609e: ("INCITS", {
        0x010483: ("SBP-2", sbp3_protocol_entries),
        0x0105bb: ("AV/C over SBP-3", sbp3_protocol_entries),
    }),
    0x00a02d: ("1394 TA", {
        0x010001: ("AV/C", {}),
        0x010002: ("CAL", {}),
        0x010004: ("EHS", {}),
        0x010008: ("HAVi", {}),
        0x014000: ("Vender Unique", {}),
        0x014001: ("Vender Unique and AV/C", {}),
        0x000100: ("IIDC 1.04", iidc104_protocol_entries),
        0x000101: ("IIDC 1.20", iidc104_protocol_entries),
        0x000102: ("IIDC 1.30", iidc131_protocol_entries),
        0x000110: ("IIDC2", iidc2_100_protocol_entries),
        0x0A6BE2: ("DPP 1.0", dpp111_protocol_entries),
        0x4B661F: ("IICP 1.0", iicp_protocol_entries),
    }),
    # vendor-defined
    0x000595: ("Alesis Corporation", {
        0x000001: ("audio", {}),
    }),
    0x000a27: ("Apple Computer, Inc.", {
        0x000010: ("iSight audio unit", isight_audio_protocol_entries),
        0x000011: ("iSight factory unit", {}),
        0x000012: ("iSight iris unit", isight_iris_protocol_entries),
    }),
    0x00d04b: ("La Cie Group S.A.", {
        0x484944: ("HID", {}),
    }),
}

def proto_of(spec, ver):
    return protocols[spec][1][ver] \
           if spec in protocols and ver in protocols[spec][1] \
           else (None, {})

def oui_of(ouidb, dummy, v):
    return ": " + protocols[v][0] if v in protocols \
           else ": " + ouidb[v] if v in ouidb \
           else ""

ieee1212_key_ids2 = {
    0x03: oui_of,
    0x0c: lambda ouidb, spec, v: " per IEEE 1394" if v & 0x83c0 == 0x83c0 \
        else "",
    0x12: oui_of,
    0x13: lambda ouidb, spec, v: ": " + protocols[spec][1][v][0] \
        if spec in protocols and v in protocols[spec][1] \
        else "",
    0x1c: oui_of,
}

ieee1212_key_ids = {
    0x01: "descriptor",
    0x02: "bus dependent info",
    0x03: "vendor",
    0x04: "hardware version",
    0x07: "module",
    0x0c: "node capabilities",
    0x0d: "eui-64",
    0x11: "unit",
    0x12: "specifier id",
    0x13: "version",
    0x14: "dependent info",
    0x15: "unit location",
    0x17: "model",
    0x18: "instance",
    0x19: "keyword",
    0x1a: "feature",
    0x1b: "extended rom",
    0x1c: "extended key specifier id",
    0x1d: "extended key",
    0x1e: "extended data",
    0x1f: "modifiable descriptor",
    0x20: "directory id",
    0x21: "revision",
}

ieee1212_types = {
    0: "immediate",
    1: "csr offset",
    2: "leaf",
    3: "directory",
}

def write_directory(f, ouidb, rom, blocks, o, i, end, context):
    dummy, dummy, dummy, spec, ver = context
    proto = proto_of(spec, ver)
    while i <= end:
        r = rom[i]
        t = r >> 30
        k = r >> 24 & 0x3f
        v = r & 0xffffff
        if t == 0:
            if k == 0x12:
                spec  = v
                proto = proto_of(spec, ver)
            elif k == 0x13:
                ver   = v
                proto = proto_of(spec, ver)
            headline = proto[0] + " " + proto[1][k](v) if k in proto[1] \
                       else ieee1212_key_ids[k] + \
                       ieee1212_key_ids2[k](ouidb, spec, v) if k in ieee1212_key_ids2 \
                       else ieee1212_key_ids[k] if k in ieee1212_key_ids \
                       else "(immediate value)"
        elif t == 1:
            headline = proto[0] + " " + proto[1][r >> 24](v) if r >> 24 in proto[1] \
                       else "CSR at %012x" % (csr_address(v))
        else:
            headline = proto[0] + " " + proto[1][r >> 24](v) + " at %x" % \
                       (o + 4 * v) if r >> 24 in proto[1] \
                       else "%s%s at %x" % \
                       (ieee1212_key_ids[k] + " " if k in ieee1212_key_ids
                        else "", ieee1212_types[t], o + 4 * v)
            blocks[i + v] = (headline, t, k, spec, ver)
        f.write("%x  %08x  %s%s\n" % (o, r, "--> " if t else "", headline))
        i += 1
        o += 4
    return i

def write_eui64_hi(f, ouidb, rom, o, i):
    hi = rom[i]
    f.write("%x  %08x  company_id %06x     | %s\n" %
            (o, hi, hi >> 8, ouidb.get(hi >> 8, "")))

def write_eui64_lo(f, ouidb, rom, o, i):
    hi = rom[i - 1]
    lo = rom[i]
    f.write("%x  %08x  device_id %02x%08x  | EUI-64 %08x%08x\n" %
            (o, lo, hi & 0xff, lo, hi, lo))

def write_leaf(f, ouidb, rom, o, i, end, context):
    dummy, dummy, key_id, spec, ver = context
    descriptor_type_and_spec = None
    is_minimal_ascii = None
    j = 0
    while i <= end:
        r = rom[i]
        if spec == 0x00a02d and ver in (0x000100, 0x000101, 0x000102) and \
           key_id in (0x01, 0x02) and j < 2:  # IIDC vendor or model name
            is_minimal_ascii = j and not r and not rom[i - 1];
            f.write("%x  %08x\n" % (o, r))
        elif key_id == 0x01 and j == 0:  # descriptor leaf, general header
            descriptor_type_and_spec = r
            if descriptor_type_and_spec == 0x00000000:
                f.write("%x  %08x  textual descriptor\n" % (o, r))
            elif descriptor_type_and_spec == 0x01000000:
                f.write("%x  %08x  icon descriptor\n" % (o, r))
            else:
                f.write("%x  %08x  descriptor_type %02x, specifier_ID %x\n" %
                        (o, r, r >> 24, r & 0xffffff))
        elif key_id == 0x1f:             # modifiable descriptor
            if j == 0:
                f.write("%x  %08x  max_descriptor_size %d, "
                        "descriptor_address_hi %d\n" %
                        (o, r, r >> 16, r & 0xffff))
            elif j == 1:
                f.write("%x  %08x  descriptor_address_lo %d\n" % (o, r, r))
        elif (key_id == 0x07 or          # primary node unique ID leaf
              key_id == 0x0d) and j < 2: # eui-64 leaf
            (write_eui64_hi, write_eui64_lo)[j](f, ouidb, rom, o, i)
        elif key_id == 0x19:             # keyword leaf
            f.write("%x  %08x  %s\n" % (o, r, u32_to_string(r)))
        elif descriptor_type_and_spec == 0 and j == 1:  # textual descriptor
            is_minimal_ascii = r >> 16 == 0
            if is_minimal_ascii:
                f.write("%x  %08x  minimal ASCII\n" % (o, r))
            else:
                f.write("%x  %08x  width %d, character_set %d, language %s\n" %
                        (o, r, r >> 28, r >> 16 & 0xfff, language(r)))
        elif is_minimal_ascii:
            f.write("%x  %08x  %s\n" % (o, r, u32_to_string(r)))
        else:
            f.write("%x  %08x\n" % (o, r))
        i += 1
        j += 1
        o += 4
    return i

def write_block(f, ouidb, rom, blocks, i, context):
    headline, t, k, spec, ver = context
    r = rom[i]
    o = 0x400 + i * 4
    l = r >> 16
    c = r & 0xffff
    crc = crc16(rom, i + 1, l)
    f.write("               %s\n"
            "               -----------------------------------------------------------------\n"
            "%x  %08x  %s_length %d, crc %d%s\n" %
            (headline, o, r, ieee1212_types[t], l, c,
             " (should be %d)" % (crc) if crc <> c else ""))
    end = min(i + l, 255)
    i += 1
    o += 4
    if t == 3:
        i = write_directory(f, ouidb, rom, blocks, o, i, end, context)
    else:
        i = write_leaf(f, ouidb, rom, o, i, end, context)
    return i

def write_bus_info_block(f, ouidb, rom, blocks):
    f.write("               ROM header and bus information block\n"
            "               -----------------------------------------------------------------\n")
    r = rom[0]
    bib_len = r >> 24
    crc_len = r >> 16 & 0xff
    c = r & 0xffff
    crc = crc16(rom, 1, crc_len)
    f.write("400  %08x  bus_info_length %d, crc_length %d, crc %d%s\n" %
            (r, bib_len, crc_len, c,
             " (should be %d)" % (crc) if crc <> c else ""))
    r = rom[1]
    f.write("404  %08x  bus_name %s\n" % (r, u32_to_string(r)))
    if r == 0x31333934:
        r = rom[2]
        gen = r >> 4 & 0xf
        if gen:
            f.write("408  %08x  irmc %d, cmc %d, isc %d, bmc %d, pmc %d, "
                    "cyc_clk_acc %d,\n               max_rec %d (%d), "
                    "max_rom %d, gen %d, spd %d (S%d00)\n" %
                    (r, r >> 31, r >> 30 & 1, r >> 29 & 1, r >> 28 & 1, r >> 27 & 1,
                     r >> 16 & 0xff, r >> 12 & 0xf, 2 << (r >> 12 & 0xf),
                     r >> 8 & 3, gen, r & 7, 1 << (r & 7)))
        else:
            f.write("408  %08x  irmc %d, cmc %d, isc %d, bmc %d, "
                    "cyc_clk_acc %d, max_rec %d (%d)\n" %
                    (r, r >> 31, r >> 30 & 1, r >> 29 & 1, r >> 28 & 1,
                     r >> 16 & 0xff, r >> 12 & 0xf, 2 << (r >> 12 & 0xf)))
    else:
        f.write("408  %08x  bus-dependent information\n" % (rom[2]))
    write_eui64_hi(f, ouidb, rom, 0x40c, 3)
    write_eui64_lo(f, ouidb, rom, 0x410, 4)
    i = 5
    while i <= bib_len:
        f.write("%x  %08x  bus-dependent information\n" %
                (0x400 + i * 4, rom[i]))
        i += 1
    blocks[i] = ("root directory", 3, None, None, None)
    return i

def write_config_rom(f, ouidb, rom):
    blocks = {}
    i = write_bus_info_block(f, ouidb, rom, blocks)
    f.write("\n")
    need_linefeed = False
    while i < 256:
        if i in blocks:
            if need_linefeed:
                f.write("\n")
            i = write_block(f, ouidb, rom, blocks, i, blocks[i])
            f.write("\n")
            need_linefeed = False
        elif rom[i]:
            f.write("%x  %08x  (unreferenced data)\n" %
                    (0x400 + i * 4, rom[i]))
            need_linefeed = True
            i += 1
        else:
            if need_linefeed:
                f.write("\n")
            need_linefeed = False
            i += 1
    if need_linefeed:
        f.write("\n")

def read_le32_data(s, l, rom):
    for i in xrange(0, l / 4):
        j = i * 4
        rom[i] = (ord(s[j    ])      ) + \
                 (ord(s[j + 1]) <<  8) + \
                 (ord(s[j + 2]) << 16) + \
                 (ord(s[j + 3]) << 24)

def read_be32_data(s, l, rom):
    for i in xrange(0, l / 4):
        j = i * 4
        rom[i] = (ord(s[j    ]) << 24) + \
                 (ord(s[j + 1]) << 16) + \
                 (ord(s[j + 2]) <<  8) + \
                 (ord(s[j + 3])      )

def read_firecontrol_output(s, rom):
    i = -1
    for line in s.splitlines():
        # fixme: should check that node IDs stay the same and no resets happened
        if line[0:18] == "reading from node ":
            j = int(line[-20:-8], 16)
            if j < 0xfffff0000400 or j >= 0xfffff0000800:
                i = -1
                continue
            i = (j - 0xfffff0000400) / 4
            if i == 0:
                rom[:] = [0] * 256
            continue
        if len(line) == 11 and \
           line[2:3] == " " and line[5:6] == " " and line[8:9] == " " and \
           i > -1:
            b1 = int(line[0:2], 16)
            b2 = int(line[3:5], 16)
            b3 = int(line[6:8], 16)
            b4 = int(line[9:11], 16)
            rom[i] = (b1 << 24) + (b2 << 16) + (b3 << 8) + b4

def read_log_data(s, rom):
    i = -1
    for line in s.splitlines():
        # fixme: should take node IDs and transaction labels into account
        if line.find("firewire_ohci: AT spd ") >= 0 and \
           line.find(", ack_pending , QR req, fffff0000") >= 0:
            j = int(line[-12:], 16)
            if j < 0xfffff0000400 or j >= 0xfffff0000800:
                i = -1
                continue
            i = (j - 0xfffff0000400) / 4
            if i == 0:
                rom[:] = [0] * 256
            continue
        if line.find("firewire_ohci: AR spd ") >= 0 and \
           line.find(", ack_complete, QR resp = ") >= 0 and \
           i > -1:
            rom[i] = int(line[-8:], 16)

def build_config_rom(f):
    rom = [0] * 256
    s = f.read()
    l = len(s)
    may_be_binary = l > 20 and l <= 1024 and l & 3 == 0
    if   may_be_binary and s[4:8] == "4931":
        read_le32_data(s, l, rom)
    elif may_be_binary and s[4:8] == "1394":
        read_be32_data(s, l, rom)
    elif l > 20 and s[0:12] == "firecontrol ":
        read_firecontrol_output(s, rom)
    else:
        read_log_data(s, rom)
    return rom

def build_oui24_db(paths):
    for p in paths:
        try:
            return dict(map(lambda l: (int(l[:6], 16), l[7:-1]), file(p, "r")))
        except (IOError, ValueError):
            continue
    # fallback: specifiers from the protocols dictionary
    return dict(map(lambda proto: (proto[0], proto[1][0]), protocols.iteritems()))

def main():
    import sys
    rom = build_config_rom(sys.stdin)
    if not rom[0] >> 24:
        return "Nothing read."
    ouidb = build_oui24_db(ouidb_search_paths)
    write_config_rom(sys.stdout, ouidb, rom)

if __name__ == "__main__":
    import sys
    sys.exit(main())
